/*
 * Copyright (c) 2006 Charles S. Wilson
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a 
 * copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the 
 * Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included 
 * in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 
 * OTHER DEALINGS IN THE SOFTWARE.
 */

#if HAVE_CONFIG_H
# include <config.h>
#endif

#if STDC_HEADERS
# include <stdlib.h>
# include <stdarg.h>
# include <string.h>
# include <float.h>
#endif

#include <stdio.h>
#include <errno.h>

#if HAVE_SYS_TYPES_H
# include <sys/types.h>
#endif
#if HAVE_MALLOC_H
# include <malloc.h>
#endif
#if HAVE_SYS_STAT_H
# include <sys/stat.h>
#endif

#if HAVE_SYS_CYGWIN_H
# include <sys/cygwin.h>
#endif
#if HAVE_DLFCN_H
# include <dlfcn.h>
#endif
#if HAVE_X11_XLIB_H
# include <X11/Xlib.h>
#endif
#if HAVE_LOCALE_H
# include <locale.h>
#endif
#if HAVE_PTHREAD
# include <pthread.h>
#endif
#if HAVE_MATH_H
# include <math.h>
#endif
#if HAVE_X11_XLIB_H
# include <X11/Xlib.h>
#endif

#if HAVE_WINDOWS_H && HAVE_OPENCLIPBOARD
# define WIN32_LEAD_AND_MEAN
# define NOMINMAX
# include <windows.h>
#endif


#include "checkX.h"
#include "util.h"


#define XSERV_TIMEDOUT -1
#define XSERV_FOUND     0
#define XSERV_NOTFOUND  1

typedef Display*(*XOpenDisplayFP)(char*);
typedef int(*XCloseDisplayFP)(Display *);
typedef struct WorkerThreadData_ {
  XOpenDisplayFP  xopendis;
  XCloseDisplayFP xclosedis;
  char*           displayname;
} WorkerThreadData;

#define timerspec_add(a, b, result)                                           \
  do {                                                                        \
    (result)->tv_sec = (a)->tv_sec + (b)->tv_sec;                             \
    (result)->tv_nsec = (a)->tv_nsec + (b)->tv_nsec;                          \
    if ((result)->tv_nsec >= 1000000000)                                      \
      {                                                                       \
        ++(result)->tv_sec;                                                   \
        (result)->tv_nsec -= 1000000000;                                      \
      }                                                                       \
  } while (0)


static pthread_mutex_t mtx_xopenOK;
static pthread_cond_t  cv_xopenOK;
static int xopenOK;
static int xopenTrying;
static const char* XLIBfmt = "cygX11-%d.dll";
static const char* DefaultAppendPath = "/usr/X11R6/bin" SEP_CHAR "/usr/bin";

/**********************************************************/
static int
adjust_path(const char*  prepend,
            const char*  append,
            const char*  replace,
                  char** newPath);

static int
find_X11_lib(const char*  searchPath,
             const char*  explicitName,
                   char** pathToX11Lib);

static int
dlopen_X11_lib(const char* pathToX11Lib,
               void**      handleToDll);

static int
load_X11_symbols(void*            handleToDll,
                 XOpenDisplayFP*  xopendisP,
                 XCloseDisplayFP* xclosedisP);

static int
test_Xserver(const char*     displayName,
             XOpenDisplayFP  xopendis,
             XCloseDisplayFP xclosedis,
             double          timeout);

static void computeTimespec(double val, timespec_t* t);
static int try_with_timeout(WorkerThreadData* data, double delay);
static void* open_display(void* /* WorkerThreadData* */ v);
/**********************************************************/

static int
adjust_path(const char*  prepend,
            const char*  append,
            const char*  replace,
                  char** newPath)
{
  char* appendPath;
  char* origPath;
  int c;
  char* path;

  if (!append)
    appendPath = run2_strdup(DefaultAppendPath);
  else
    appendPath = run2_strdup(append);

  origPath = getenv("PATH");
  c = strlen(origPath);
  if (replace)     { c  = strlen(replace);         }
  if (appendPath)  { c += strlen(appendPath) + 1;  }
  if (prepend)     { c += strlen(prepend) + 1;     }

  path = (char*) run2_malloc (c * sizeof(char*));
  path[0] = '\0';
  if (prepend) {
    strcat(path, prepend);
    strcat(path, SEP_CHAR);
  }
  if (replace) {
    strcat(path, replace);
  } else {
    strcat(path, origPath);
  }
  if (appendPath) {
    strcat(path, SEP_CHAR);
    strcat(path, appendPath);
  }
  debugMsg(1, "(%s) path is : %s", __func__, path);

  *newPath = path;
  return 0;
}

static int
find_X11_lib(const char*  searchPath,
             const char*  explicitName,
                   char** pathToX11Lib)
{
  char bufFullPath[MAX_PATH + FILENAME_MAX + 100];
  char bufFileName[FILENAME_MAX];
  int i;

  if (explicitName) {
    run2_pfopen(bufFullPath, explicitName, searchPath);
  } else {
    for (i = 9; i > 5; i--) {
      sprintf(bufFileName, XLIBfmt, i);
      run2_pfopen(bufFullPath, bufFileName, searchPath);
      if (run2_fileExists(NULL,NULL,bufFullPath))
         break;
      bufFullPath[0] = '\0';
    }
  }
  if (bufFullPath[0] == '\0') {
    errorMsg("could not locate Xlib DLL %s", (explicitName ? explicitName : XLIBfmt));
    return 1;
  }
  *pathToX11Lib = run2_strdup(bufFullPath);
  debugMsg(1, "(%s) DLL is %s", __func__, *pathToX11Lib);

  return 0;
}

static int
dlopen_X11_lib(const char* pathToX11Lib,
               void**      handleToDll)
{
  void* handle = NULL;

  handle = dlopen(pathToX11Lib, RTLD_LAZY | RTLD_GLOBAL);
  if (!handle) {
    errorMsg("problem loading %s: %s", pathToX11Lib, dlerror());
    return 1;
  }
  debugMsg(1, "(%s) %s dlopen'ed successfully.", __func__, pathToX11Lib);
  *handleToDll = handle;
  return 0;
}

static int
load_X11_symbols(void*            handleToDll,
                 XOpenDisplayFP*  xopendisP,
                 XCloseDisplayFP* xclosedisP)
{
  XOpenDisplayFP xopendis = (XOpenDisplayFP)NULL;
  XCloseDisplayFP xclosedis = (XCloseDisplayFP)NULL;

  xopendis =  (XOpenDisplayFP) dlsym(handleToDll, "XOpenDisplay");
  if (!xopendis)
  {
    errorMsg("problem loading symbol XOpenDisplay: %s", dlerror());
    return 1;
  }
  debugMsg(1, "(%s) symbol XOpenDisplay loaded ok", __func__);

  xclosedis = (XCloseDisplayFP)dlsym(handleToDll, "XCloseDisplay");
  if (!xclosedis)
  {
    errorMsg("problem loading symbol XCloseDisplay: %s", dlerror());
    return 1;
  }
  debugMsg(1, "(%s) symbol XCloseDisplay loaded ok", __func__);

  *xopendisP = xopendis;
  *xclosedisP = xclosedis;
  return 0;
}

static int
test_Xserver(const char*     displayName,
             XOpenDisplayFP  xopendis,
             XCloseDisplayFP xclosedis,
             double          timeout)
{
  Display* dpy = NULL;
  WorkerThreadData data;
  int rc;
  char* dispName;

  if (displayName)
  {
    dispName = run2_strdup(displayName);
  }
  else
  {
    dispName = NULL;
  }
  data.xopendis = xopendis;
  data.xclosedis = xclosedis;
  data.displayname = dispName;

  rc = try_with_timeout(&data, timeout);

  if (rc == 0) {
    infoMsg("X display '%s' successfully opened",
             (displayName ? displayName : ""));
  } else {
    infoMsg("could not open X display '%s'",
            (displayName ? displayName : ""));
  }

  if (dispName) free(dispName);
  return rc;
}

static void
computeTimespec(double val, timespec_t* t)
{
   double frac;
   if (!t) return;
   if (val < 0) return;
   t->tv_sec = (time_t)(floor(val));
   frac = (val - (double)t->tv_sec);
   t->tv_nsec = (long)(1000000000.0 * frac);
}

static int
try_with_timeout(WorkerThreadData* data, double delay)
{
  pthread_attr_t attr;
  pthread_t      id;
  int            status;
  timespec_t     now;
  timespec_t     delta;
  timespec_t     then;

  xopenTrying = delay!=0.0; /* false actually means: try once */
  xopenOK = XSERV_NOTFOUND; /* a pessimistic start out */

  computeTimespec(fabs(delay), &delta);
  debugMsg(1, "(%s) Using delay of %d secs, %ld nanosecs (%5.2f)", __func__,
           delta.tv_sec, delta.tv_nsec,
           (double)delta.tv_sec + ((double)delta.tv_nsec)/1000000000.0);

  pthread_mutex_init(&mtx_xopenOK, NULL);
  pthread_cond_init (&cv_xopenOK, NULL);

  pthread_attr_init(&attr);
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);

  pthread_mutex_lock(&mtx_xopenOK);

  pthread_create(&id, &attr, open_display, (void*)data);
  pthread_attr_destroy(&attr);

  if (delay != 0.0) {
    clock_gettime(CLOCK_REALTIME, &now);
    timerspec_add(&now, &delta, &then);
    if (pthread_cond_timedwait (&cv_xopenOK, &mtx_xopenOK, &then) == ETIMEDOUT) {
      xopenOK = XSERV_TIMEDOUT; /* it's okay, we have the mutex */
      xopenTrying = 0;  /* allow open_display() to give up */
    }    /* else open_display() was successful */
    pthread_detach(id);  /* leave open_display() on its own */
  } else {
    pthread_mutex_unlock(&mtx_xopenOK); /* allow open_display() to set xopenOK */
    pthread_join(id, (void*)&status); /* and wait for it */
  }

  pthread_mutex_destroy(&mtx_xopenOK);
  debugMsg(1, "(%s) xserver search was %s", __func__,
           (xopenOK == XSERV_TIMEDOUT ? "timed out" :
           (xopenOK == XSERV_NOTFOUND ? "unsuccessful" : "a success!")));

  // translate timeout into not-found
  return (xopenOK == XSERV_TIMEDOUT ? XSERV_NOTFOUND : xopenOK );
}

static void*
open_display(void* /* WorkerThreadData* */ v)
{
  Display* dpy;
  WorkerThreadData* data = (WorkerThreadData*)v;

  do
    if((dpy = (*(data->xopendis))(data->displayname))) {
      (*(data->xclosedis))(dpy);
      pthread_mutex_lock (&mtx_xopenOK);
      xopenOK = XSERV_FOUND;
      pthread_cond_signal(&cv_xopenOK);
      pthread_mutex_unlock (&mtx_xopenOK);
    }
  while (xopenTrying && xopenOK == XSERV_NOTFOUND);

  pthread_exit((void*)0);
}

int
run2_checkX(const char* prepend,       /* can be NULL */
            const char* append,        /* can be NULL - uses default value */
            const char* replace,       /* can be NULL */
            const char* explicitName,  /* can be NULL - uses search pattern */
            const char* displayName,   /* can be NULL - XOpenDisplay defaults to :0 */
            double      timeout,       /* 0 means XOpenDisplay's builtin timeout of 12 s */
            char**      pathToX11Lib)  /* if not NULL, caller must free */
{
  int rc;
  char* pathList = NULL;
  char* x11LibPath = NULL;
  void* handleToDll = NULL;
  XOpenDisplayFP xopendis = NULL;
  XCloseDisplayFP xclosedis = NULL;
  const char* dispVar;

  /***********************
   * PATH handling
   ***********************/
  rc = adjust_path(prepend,
                   append,
                   replace,
                   &pathList);
  if (rc != 0)
  {
    return 1;
  }

  /***********************
   * XLIB library name
   ***********************/
  rc = find_X11_lib(pathList,
                    explicitName,
                    &x11LibPath);
  free(pathList);
  pathList = NULL;
  if (rc != 0)
  {
    return 1;
  }

  /***********************
   * XLIB library load
   ***********************/
  rc = dlopen_X11_lib(x11LibPath, &handleToDll);
  if (rc != 0)
  {
    free(x11LibPath);
    return 1;
  }

  /***********************
   * XLIB symbol load
   ***********************/
  rc = load_X11_symbols(handleToDll, &xopendis, &xclosedis);
  if (rc != 0)
  {
    free(x11LibPath);
    dlclose(handleToDll);
    return 1;
  }

  /***********************
   * Test XServer
   ***********************/
  dispVar = displayName ? displayName : getenv("DISPLAY");
  rc = test_Xserver(dispVar,
                    xopendis,
                    xclosedis,
                    timeout);
  dlclose(handleToDll);
  if (rc != 0)
  {
    free(x11LibPath);
    return 1;
  }

  if (pathToX11Lib)
  {
    *pathToX11Lib = x11LibPath;
  }
  else
  {
    free(x11LibPath);
  }
  return 0;
}

